// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.dfp.jaxws.utils.v201208;

import com.google.api.ads.dfp.jaxws.v201208.BooleanValue;
import com.google.api.ads.dfp.jaxws.v201208.ColumnType;
import com.google.api.ads.dfp.jaxws.v201208.DateTime;
import com.google.api.ads.dfp.jaxws.v201208.DateTimeValue;
import com.google.api.ads.dfp.jaxws.v201208.NumberValue;
import com.google.api.ads.dfp.jaxws.v201208.ResultSet;
import com.google.api.ads.dfp.jaxws.v201208.Row;
import com.google.api.ads.dfp.jaxws.v201208.TextValue;
import com.google.api.ads.dfp.jaxws.v201208.Value;
import com.google.common.collect.Lists;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.reflect.MethodUtils;

import java.lang.reflect.InvocationTargetException;
import java.util.List;

import javax.annotation.Nullable;

/**
 * A utility class for handling PQL objects.
 *
 * @author Adam Rogal.
 */
public final class Pql {

  /**
   * {@code Pql} is meant to be used statically.
   */
  private Pql() {}

  /**
   * Creates a {@link Value} from the value i.e. a {@link TextValue} for a
   * value of type {@code String}, {@link BooleanValue} for type
   * {@code Boolean}, {@link NumberValue} for type {@code Double},
   * {@code Long}, or {@code Integer}, and {@link DateTimeValue} for type
   * {@link DateTime}. If the value is a {@code Value}, the value is returned.
   * If the value is {@code null}, an empty {@link TextValue} is returned.
   *
   * @param value the value to convert
   * @return the constructed value of the appropriate type
   * @throws IllegalArgumentException if value cannot be converted
   */
  public static Value createValue(Object value) {
    if (value instanceof Value) {
      return (Value) value;
    } else if (value == null) {
      return new TextValue();
    } else {
      if (value instanceof Boolean) {
        BooleanValue booleanValue = new BooleanValue();
        booleanValue.setValue((Boolean) value);
        return booleanValue;
      } else if (value instanceof Double || value instanceof Long || value instanceof Integer) {
        NumberValue numberValue = new NumberValue();
        numberValue.setValue(value.toString());
        return numberValue;
      } else if (value instanceof String) {
        TextValue textValue = new TextValue();
        textValue.setValue((String) value);
        return textValue;
      } else if (value instanceof DateTime) {
        DateTimeValue dateTimeValue = new DateTimeValue();
        dateTimeValue.setValue((DateTime) value);
        return dateTimeValue;
      } else {
        throw new IllegalArgumentException("Unsupported Value type [" + value.getClass() + "]");
      }
    }
  }

  /**
   * Gets the string value equivalent of the supplied value.
   *
   * @param value the {@code Value} to get the string from
   * @return the string value equivalent or {@code null} if it's {@code null}
   * @throws IllegalArgumentException if the value could not be extracted from
   *         the row value
   * @throws IllegalAccessException if the value could not be accessed
   * @throws NoSuchMethodException if the value could not be accessed
   * @throws InvocationTargetException if the value could not be accessed
   */
  @Nullable
  public static String getValueString(@Nullable Value value) throws IllegalArgumentException,
      IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object internalValue = null;
    if (value instanceof BooleanValue) {
      internalValue = MethodUtils.invokeExactMethod(value, "isValue", null);
    } else {
      internalValue = MethodUtils.invokeExactMethod(value, "getValue", null);
    }
    return (internalValue == null) ? null : internalValue.toString();
  }

  /**
   * Gets the result set as list of string arrays, which can be transformed to a CSV using
   * {@code CSVWriter}.
   *
   * @param resultSet the result set to convert to a CSV compatible format
   * @return a list of string arrays representing the result set
   */
  public static List<String[]> resultSetToStringArrayList(ResultSet resultSet) {
    List<String[]> stringArrayList = Lists.newArrayList();
    stringArrayList.add(getColumnLabels(resultSet));
    if (resultSet.getRows() != null) {
      for (Row row : resultSet.getRows()) {
        try {
          stringArrayList.add(getRowStringValues(row));
        } catch (IllegalArgumentException e) {
          throw new IllegalStateException("Cannot convert result set to string array list", e);
        } catch (IllegalAccessException e) {
          throw new IllegalStateException("Cannot convert result set to string array list", e);
        } catch (InvocationTargetException e) {
          throw new IllegalStateException("Cannot convert result set to string array list: "
              + e.getMessage(), e.getTargetException());
        } catch (NoSuchMethodException e) {
          throw new IllegalStateException("Cannot convert result set to string array list", e);
        }
      }
    }
    return stringArrayList;
  }

  /**
   * Gets the result set as a table representation in the form of:
   *
   * <pre>
   * +-------+-------+-------+
   * |column1|column2|column3|
   * +-------+-------+-------+
   * |value1 |value2 |value3 |
   * +-------+-------+-------+
   * |value1 |value2 |value3 |
   * +-------+-------+-------+
   * </pre>
   *
   * @param resultSet the result set to display as a string
   * @return the string representation of result set as a table
   * @throws IllegalAccessException if the values of the result set cannot be accessed
   */
  public static String resultSetToString(ResultSet resultSet) throws IllegalAccessException {
    StringBuilder resultSetStringBuilder = new StringBuilder();
    List<String[]> resultSetStringArrayList = resultSetToStringArrayList(resultSet);
    List<Integer> maxColumnSizes = getMaxColumnSizes(resultSetStringArrayList);
    String rowTemplate = createRowTemplate(maxColumnSizes);
    String rowSeparator = createRowSeperator(maxColumnSizes);

    resultSetStringBuilder.append(rowSeparator);
    for (int i = 0; i < resultSetStringArrayList.size(); i++) {
      resultSetStringBuilder.append(
          String.format(rowTemplate, (Object[]) resultSetStringArrayList.get(i))).append(
          rowSeparator);
    }
    return resultSetStringBuilder.toString();
  }

  /**
   * Creates the row template given the maximum size for each column
   *
   * @param maxColumnSizes the maximum size for each column
   * @return the row template to format row data into
   */
  private static String createRowTemplate(List<Integer> maxColumnSizes) {
    List<String> columnFormatSpecifiers = Lists.newArrayList();
    for (int maxColumnSize : maxColumnSizes) {
      columnFormatSpecifiers.add("%-" + maxColumnSize + "s");
    }
    return new StringBuilder("| ").append(StringUtils.join(columnFormatSpecifiers, " | "))
        .append(" |\n").toString();
  }

  /**
   * Creates the row separator given the maximum size for each column
   *
   * @param maxColumnSizes the maximum size for each column
   * @return the row separator
   */
  private static String createRowSeperator(List<Integer> maxColumnSizes) {
    StringBuilder rowSeparator = new StringBuilder("+");
    for (int maxColumnSize : maxColumnSizes) {
      rowSeparator.append(StringUtils.repeat("-", maxColumnSize + 2)).append("+");
    }
    return rowSeparator.append("\n").toString();
  }

  /**
   * Gets a list of the maximum size for each column.
   *
   * @param resultSet the result set to process
   * @return a list of the maximum size for each column
   */
  private static List<Integer> getMaxColumnSizes(List<String[]> resultSet) {
    List<Integer> maxColumnSizes = Lists.newArrayList();
    for (int i = 0; i < resultSet.get(0).length; i++) {
      int maxColumnSize = -1;
      for (int j = 0; j < resultSet.size(); j++) {
        if (resultSet.get(j)[i].length() > maxColumnSize) {
          maxColumnSize = resultSet.get(j)[i].length();
        }
      }
      maxColumnSizes.add(maxColumnSize);

    }
    return maxColumnSizes;
  }

  /**
   * Gets the column labels for the result set.
   *
   * @param resultSet the result set to get the column labels for
   * @return the string array of column labels
   */
  public static String[] getColumnLabels(ResultSet resultSet) {
    List<String> columnLabels = Lists.newArrayList();
    for (ColumnType column : resultSet.getColumnTypes()) {
      columnLabels.add(column.getLabelName());
    }
    return columnLabels.toArray(new String[] {});
  }

  /**
   * Gets the values in a row of the result set in the form of an object array.
   *
   * @param row the row to get the values for
   * @return the object array of the row values
   * @throws IllegalArgumentException if the value could not be extracted from the row value
   * @throws IllegalAccessException if the row value could not be accessed
   * @throws NoSuchMethodException if the row value could not be accessed
   * @throws InvocationTargetException if the row value could not be accessed
   */
  public static Object[] getRowValues(Row row) throws IllegalArgumentException,
      IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    List<Object> rowValues = Lists.newArrayList();
    for (Value value : row.getValues()) {
      rowValues.add(getValueString(value));
    }
    return rowValues.toArray(new Object[] {});
  }

  /**
   * Gets the values in a row of the result set in the form of a string array. {@code null} values
   * are interpreted as empty strings.
   *
   * @param row the row to get the values for
   * @return the string array of the row values
   * @throws IllegalArgumentException if the value could not be extracted from the row value
   * @throws IllegalAccessException if the row value could not be accessed
   * @throws NoSuchMethodException if the row value could not be accessed
   * @throws InvocationTargetException if the row value could not be accessed
   */
  public static String[] getRowStringValues(Row row) throws IllegalArgumentException,
      IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object[] rowValues = getRowValues(row);
    List<String> rowStringValues = Lists.newArrayList();
    for (Object obj : rowValues) {
      if (obj != null) {
        rowStringValues.add(obj.toString());
      } else {
        rowStringValues.add("");
      }
    }
    return rowStringValues.toArray(new String[] {});
  }
}
